Készítők: Csilling Tamás és Knyihár Gábor
Azonosító: BKK3
Az I épületből este 10-kor, tömegközlekedéssel elérhető "kocsmák" (i.e., Google Maps találat erre a keresésre) elemzése
https://openmobilitydata.org/p/bkk/42 és pl. Google Places API
A Google Places API (a free változat elég kell, hogy legyen) talán a fő kihívás, egyenértékes megoldást elfogadok
Valószínűleg nem szükséges
Az I épület 10 perces sétatávolságában megállóval rendelkező éjszakai járatok alapvető jellemzői (javasolt: pandas_profiling vagy BambooLib)
Datashader alapú (lehet HoloViz-be integrálva) interaktív, Budapest-térkép alapú heatmap, mely az I épületből este 10-es indulással, átszállás nélkül való utazási időben vett távolságát mutatja a "celláknak" (cellán belül átlagolással, ha több opció is van). Természetesen vannak a városnak olyan részei, ahova átszállás nélkül nem lehet eljutni az egyetemről!
Algoritmus azon megállók megtalálására, ahova 45 perc alatt, max k átszállással el lehet jutni az I épületből este 10-es indulással. (Demonstráció elég k=1 vagy 2-re.)
Az adatok elérhetőek a https://www.bkk.hu/gtfs/budapest_gtfs.zip linken, melyek GTFS formátumban vannak. Az elérhető adatok struktúráját mutatja a következő ábra:

Ezek számos felesleges adatot is tartalmaznak, így ezt átalakítottuk és összefésültük egy nagy adathalmazba. Az oszlopok megfeleltetéseit mutatja a következő táblázat.
| Változó név | Eredeti tábla | Eredeti név |
|---|---|---|
| agency_name | agency | agency_name |
| route_name | routes | route_short_name |
| route_type | routes | route_type |
| route_desc | routes | route_desc |
| trip_id | trips | trip_id |
| trip_direction | trips | direction_id |
| trip_wheelchair_accessible | trips | wheelchair_accessible |
| trip_bikes_allowed | trips | bikes_allowed |
| trip_boarding_door | trips | boarding_door |
| stop_arrival_time | stop_times | arrival_time |
| stop_departure_time | stop_times | departure_time |
| stop_id | stops | stop_id |
| stop_name | stops | stop_name |
| stop_latitude | stops | stop_lat |
| stop_longitude | stops | stop_lon |
| stop_location_type | stops | location_type |
| stop_wheelchair_boarding | stops | wheelchair_boarding |
Ezen felül néhány érték esetében alapértelmezetten NaN értékek kerültek be a táblázatba, ezeket át kellett alakítani, valamint kezelni kellett a időformátumokat is. A GTFS-ben az éjfél után közlekedő járatok hivatalosan még az adott naphoz tartoznak, ezért például egy 01:00-kor közlekedő járat ideje 25:00-ként van megjelölve. Ez viszont értelmezhetetlen a programkódban, így ezeket is át kellett alakítani. Viszont jelölni kellett, hogy az a következő napon van, azért, hogy könnyen lehessen kezelni az összehasonlításokat. A végeredményt mentettük parquett fájlba.
## ----- Imports -----
from datetime import datetime, timedelta
import os
import requests
import zipfile
import pandas as pd
import io
## ----- Helpers -----
def downloadFile(url, path, chunk_size=128):
"""Download zip file from url to path"""
if os.path.isfile(path):
return
r = requests.get(url, stream=True)
with open(path, 'wb') as fd:
for chunk in r.iter_content(chunk_size=chunk_size):
fd.write(chunk)
def getDataFrameFromZip(path, filename):
"""Extract pandas dataframe from zip file"""
archive = zipfile.ZipFile(path, 'r')
csv = archive.read(filename).decode("utf-8")
handler = io.StringIO(csv)
return pd.read_csv(handler,low_memory=False)
def convertTime(time):
"""Convert time string to time objects"""
hours = int(time[0:2])
if hours < 24:
return datetime.strptime(time, "%H:%M:%S")
else:
new_time = str(hours-24).zfill(2) + time[2:]
return datetime.strptime(new_time, "%H:%M:%S") + timedelta(days=1)
## ----- Variables -----
parquet_path = "temp/bkk.parquet.gzip"
## ----- Script -----
def import_bkk():
url = "https://www.bkk.hu/gtfs/budapest_gtfs.zip"
temp_folder = "temp"
zip_path = "temp/bkk.zip"
if not os.path.isdir(temp_folder):
os.mkdir(temp_folder)
if not os.path.exists(parquet_path):
# Download zip file
downloadFile(url, zip_path)
# Collect and clean data
agency = getDataFrameFromZip(zip_path, "agency.txt")[["agency_id", "agency_name"]].set_index("agency_id")
routes = getDataFrameFromZip(zip_path,"routes.txt")[["agency_id", "route_id", "route_short_name", "route_type", "route_desc"]].set_index(["route_id", "agency_id"])
routes["route_type"] = routes["route_type"].astype(int)
routes["route_type"] = routes["route_type"].replace([0,1,3,4,11,109],[0,1,2,3,4,5])
routes = routes.rename(columns={'route_short_name': 'route_name'})
trips = getDataFrameFromZip(zip_path,"trips.txt")[["route_id", "trip_id", "direction_id","wheelchair_accessible", "bikes_allowed", "boarding_door"]].set_index(["trip_id", "route_id"])
trips = trips.fillna(0)
trips["direction_id"] = trips["direction_id"].astype(int)
trips["wheelchair_accessible"] = trips["wheelchair_accessible"].astype(int)
trips["bikes_allowed"] = trips["bikes_allowed"].astype(int)
trips["boarding_door"] = trips["boarding_door"].astype(int)
trips["boarding_door"] = trips["boarding_door"].replace([0,2],[0,1])
trips = trips.rename(columns={'direction_id': 'trip_direction', 'wheelchair_accessible': 'trip_wheelchair_accessible', 'bikes_allowed':'trip_bikes_allowed', 'boarding_door': 'trip_boarding_door'})
stop_times = getDataFrameFromZip(zip_path,"stop_times.txt")[["stop_id", "trip_id", "arrival_time", "departure_time"]].set_index(["stop_id", "trip_id"])
stop_times["departure_time"] = stop_times.departure_time.apply(convertTime)
stop_times["arrival_time"] = stop_times.arrival_time.apply(convertTime)
stop_times = stop_times.rename(columns={'arrival_time': 'stop_arrival_time', 'departure_time':'stop_departure_time'})
stops = getDataFrameFromZip(zip_path,"stops.txt")[["stop_id", "stop_name", "stop_lat", "stop_lon", "location_type", "wheelchair_boarding"]].set_index("stop_id")
stops = stops.fillna(0)
stops["location_type"] = stops["location_type"].astype(int)
stops["wheelchair_boarding"] = stops["wheelchair_boarding"].astype(int)
stops = stops.rename(columns={'stop_lat': 'stop_latitude', 'stop_lon':'stop_longitude', 'location_type':'stop_location_type', 'wheelchair_boarding': 'stop_wheelchair_boarding'})
# Join data and drop unnecessary columns
all_data = trips.join(routes).join(stop_times).join(stops).join(agency)
all_data = all_data.reset_index().drop(columns=["route_id", "agency_id"])
# Order columns
all_data = all_data[["agency_name","route_name","route_type","route_desc", "trip_id", "trip_direction", "trip_wheelchair_accessible", "trip_bikes_allowed", "trip_boarding_door", "stop_arrival_time", "stop_departure_time", "stop_id", "stop_name", "stop_latitude", "stop_longitude", "stop_location_type", "stop_wheelchair_boarding"]]
# Export to parquet
all_data.to_parquet(parquet_path, compression='gzip')
else:
# Import from parquet
all_data = pd.read_parquet(parquet_path)
return all_data
df = import_bkk()
Az adatokat parquet fájlból töltjük be, mely a következő oszlopokat tartalmazza:
| Változó | Változó neve | Típus | Megengedett értékek | Leírás |
|---|---|---|---|---|
| Szolgáltató neve | agency_name | Szöveg | A szolgáltató teljes neve. | |
| Járat neve | route_name | Szöveg | Az járat rövid neve. | |
| Járat típusa | route_type | Egész szám | 0 - villamos, 1 - metró, 2 - busz, 3 - hajó, 4 - troli, 5 - hév | A járatot kiszolgáló jármű típusa. |
| Járat leírása | route_desc | Szöveg | A járat rövid leírása. | |
| Útvonal azonosítója | trip_id | Szöveg | Két megálló közötti utazás azonosítója. | |
| Útvonal iránya | trip_direction | Egész szám | 0 - normál, 1 - ellentétes | Az utazás menetirányát jelzi. |
| Kerekesszékkel elérhető | trip_wheelchair_accessible | Egész szám | 0 - ismeretlen, 1 - igen, 2 - nem | Azt jelzi, hogy a járaton kerekesszékkel lehetséges-e utazni. |
| Kerékpárok engedélyezettek | trip_bikes_allowed | Egész szám | 0 - ismeretlen, 1 - igen, 2 - nem | Azt jelzi, hogy megengedett-e a kerékpár szállítás. |
| Beszálló ajtó | trip_boarding_door | Egész szám | 0 - bármelyik, 1 - első ajtó | Azt jelzi, hogy melyik ajtón lehet-e felszállni. |
| Érkezési idő | stop_arrival_time | Idő | Érkezési idő egy adott megállóhelyen egy adott utazáshoz. | |
| Indulási idő | stop_departure_time | Idő | Indulási idő egy adott megállóból egy adott utazáshoz. | |
| Megálló azonosítója | stop_id | Szöveg | Megállóhelyet, állomást vagy állomás bejáratát azonosítja. | |
| Megálló neve | stop_name | Szöveg | A megálló neve. | |
| Megálló helye (szélesség) | stop_latitude | Szám | -90 - +90 | A megálló koordinátájának szélességi foka. |
| Megálló helye (hosszúság) | stop_longitude | Szám | -180 - +180 | A megálló kordinátájának hosszúsági foka. |
| Megálló típusa | stop_location_type | Egész szám | 0 - megálló, 1 - állomás, 2 - állomás bejárat/kijárat | A megálló típusa. |
| Kerekesszékes beszállás | stop_wheelchair_boarding | Egész szám | 0 - ismeretlen, 1 - igen, 2 - nem | Azt jelzi, hogy a megállóból lehetséges-e a kerekesszékes felszállás. |
display(df)
| agency_name | route_name | route_type | route_desc | trip_id | trip_direction | trip_wheelchair_accessible | trip_bikes_allowed | trip_boarding_door | stop_arrival_time | stop_departure_time | stop_id | stop_name | stop_latitude | stop_longitude | stop_location_type | stop_wheelchair_boarding | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | BKK | 7G | 2 | Cinkotai autóbuszgarázs / Újpalota, Nyírpalota út | B870931 | 0 | 1 | 2 | 0 | 1900-01-01 03:50:00 | 1900-01-01 03:50:00 | 008569 | Cinkotai autóbuszgarázs | 47.498051 | 19.236675 | 0 | 2 |
| 1 | BKK | 7G | 2 | Cinkotai autóbuszgarázs / Újpalota, Nyírpalota út | B870931 | 0 | 1 | 2 | 0 | 1900-01-01 03:50:00 | 1900-01-01 03:50:00 | F03291 | Injekcióüzem | 47.496206 | 19.231971 | 0 | 1 |
| 2 | BKK | 7G | 2 | Cinkotai autóbuszgarázs / Újpalota, Nyírpalota út | B870931 | 0 | 1 | 2 | 0 | 1900-01-01 03:51:00 | 1900-01-01 03:51:00 | F03403 | EGIS Gyógyszergyár | 47.497054 | 19.224595 | 0 | 1 |
| 3 | BKK | 7G | 2 | Cinkotai autóbuszgarázs / Újpalota, Nyírpalota út | B870931 | 0 | 1 | 2 | 0 | 1900-01-01 03:52:00 | 1900-01-01 03:52:00 | F03402 | Zsemlékes út | 47.503095 | 19.214434 | 0 | 1 |
| 4 | BKK | 7G | 2 | Cinkotai autóbuszgarázs / Újpalota, Nyírpalota út | B870931 | 0 | 1 | 2 | 0 | 1900-01-01 03:53:00 | 1900-01-01 03:53:00 | F03400 | Petőfi tér | 47.506473 | 19.211007 | 0 | 1 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 5723094 | MÁV-HÉV | H5 | 5 | Batthyány tér / Szentendre | H8000_22 | 0 | 0 | 0 | 0 | 1900-01-02 00:48:00 | 1900-01-02 00:48:00 | F04692 | Budakalász, Lenfonó | 47.621692 | 19.046912 | 0 | 0 |
| 5723095 | MÁV-HÉV | H5 | 5 | Batthyány tér / Szentendre | H8000_22 | 0 | 0 | 0 | 0 | 1900-01-02 00:50:00 | 1900-01-02 00:50:00 | F04793 | Szentistvántelep | 47.629301 | 19.043159 | 0 | 0 |
| 5723096 | MÁV-HÉV | H5 | 5 | Batthyány tér / Szentendre | H8000_22 | 0 | 0 | 0 | 0 | 1900-01-02 00:52:00 | 1900-01-02 00:53:00 | F04690 | Pomáz | 47.643188 | 19.032032 | 0 | 0 |
| 5723097 | MÁV-HÉV | H5 | 5 | Batthyány tér / Szentendre | H8000_22 | 0 | 0 | 0 | 0 | 1900-01-02 00:57:00 | 1900-01-02 00:57:00 | F04688 | Pannóniatelep | 47.652488 | 19.065294 | 0 | 0 |
| 5723098 | MÁV-HÉV | H5 | 5 | Batthyány tér / Szentendre | H8000_22 | 0 | 0 | 0 | 0 | 1900-01-02 01:00:00 | 1900-01-02 01:00:00 | 009273 | Szentendre | 47.661038 | 19.075607 | 0 | 0 |
5723099 rows × 17 columns
Későbbi vizualizációkhoz felhasználjuk Budapest közigazgatási határait is. A letölthető fájl egy KML, de az egyszerűség kedvéért ezt kézzel átalakítottuk CSV formátumra.
## ----- Imports -----
import matplotlib.path as mplPath
from datashader.utils import lnglat_to_meters
## ----- Variables -----
budapest_csv_path = "data/budapest.csv"
## ----- Scrips -----
# Import the border of Budapest
budapest = pd.read_csv(budapest_csv_path)
budapest["x"], budapest["y"] = lnglat_to_meters(budapest.lon, budapest.lat)
budapest_border = mplPath.Path(budapest[["x","y"]])
A Places API lehetőséget ad arra, hogy a Google maps adatait elérjük, többek között kategória szerinti keresésekkel is. A kategóriák között közvetlen meg lehet adni hogy bárokra szeretnénk keresni. Az eredeti koncepció szerint csak az elérhető megállók környékén történt volna bárokra keresés, de mivel nagyjából 2 óra tömegközlekedéssel a teljes vros bejrható, ezért a teljes városra lekérdeztük a bárokat.
Az ehhez használt API endpoint a nearbysearch volt, aminek segítségével nagyjából egy kör sugarú területen belül a keresési feltételeknek megfelelő POI-kat mind megkaphatjuk. Az a módszer, amivel a keresés történt sérti a Google Maps ToS-ét, ezért ismétlése nem ajánlott, a kódot érdeme átdolgozni a ToS-el összhangban. Ez többek között azt jelenti, hogy csak online lehet az adatokat elérni, offline tárolásuk nem megengedett.
Valószínűleg egy lehetséges alternatíva, hogy a grid mentén történő lekérdezés helyett egy körrel egész Budapestet lefedve lehet az összes bárt lekérdezni, amelyek Budapest területén találhatóak.
Az API használatához egy .env fileban, vagy környezeti változóként meg kell adni egy Google API kulcsot, ami fel van készítve Places API használatára.
Az endpointhoz egy python wrappert használtunk, ahol megoldott az Error handling, de a paging nem.
Így megkaptunk egy listát az elérhető helyekről, Place formátumban.
A Place formátum:
| Változó név | Típus | Tartalom |
|---|---|---|
| name | string | bár neve |
| business_status | string | státusz |
| geometry | Geometry | leíró geometria a helyhez |
| place_id | string | id a Place objetumhoz |
és még sok más...
Ezek a legfontosabb informácók. A Geometry típusból kinyerhetőek a koordinátái a bárnak, ami alapján el lehet őket helyezni a térképen. Ezeket az információkta át is transzformáltuk, és az így kapott végleges adatokat elmentettük Parquet formátumban.
## ----- Imports -----
import numpy as np
from src.Places import Places
## ----- Variables -----
places_parquet_path = "temp/bars.parquet.gzip"
## ----- Script -----
def import_bars():
if not os.path.exists(places_parquet_path):
raise PermissionError("This is in direct violation with Google place ToS. \n\
Use only if you accept this. The code will be rewritten to be comilant.")
# Determine the minimum and maximum coordinates
min_lon, max_lon = min(budapest.lon), max(budapest.lon)
min_lat, max_lat = min(budapest.lat), max(budapest.lat)
# Create grid
resolution = 50
range_lon = np.linspace(min_lon, max_lon, resolution)
range_lat = np.linspace(min_lat, max_lat, resolution)
places_grid = pd.DataFrame({"lat": range_lat}).join(pd.DataFrame({"lon": range_lon}), how='cross')
places_grid["x"], places_grid["y"] = lnglat_to_meters(places_grid.lon, places_grid.lat)
# Determine points inside of Budapest
places_grid = places_grid[budapest_border.contains_points(places_grid[["x", "y"]])]
if places_grid.size > 8000:
raise ValueError("May too big request, check if you would like to proceed!!")
# Use places api interface here to load all points
client = Places()
all_bars = client.get_bars_batch(places_grid, radius=425)
all_bars.drop_duplicates("place_id", inplace=True)
# Now all bars are available as points
all_bars["x"], all_bars["y"] = lnglat_to_meters(all_bars.bar_lon, all_bars.bar_lat)
all_bars.to_parquet(places_parquet_path)
else:
# Load from parquet file
all_bars = pd.read_parquet(places_parquet_path).drop_duplicates("place_id")
return all_bars
bars = import_bars()
## ----- Imports -----
import holoviews as hv
from holoviews.element.tiles import OSM
from holoviews.operation.datashader import *
import warnings
## ----- Variables -----
image_height=500
image_width=750
colormap_to_use='RdYlGn_r'
## ----- Script -----
# Initialize holoviews
warnings.filterwarnings("ignore")
hv.extension('bokeh', logo=False)
clipping = {'NaN': '#00000000'}
hv.opts.defaults(
hv.opts.Image(cmap=colormap_to_use,
height=image_height, width=image_width,
colorbar=True,
tools=['hover'], active_tools=['wheel_zoom'],
clipping_colors=clipping),
hv.opts.Tiles(active_tools=['wheel_zoom'], height=image_height, width=image_width),
hv.opts.Points(active_tools=['wheel_zoom'], height=image_height, width=image_width)
)
map_tiles = OSM()
map_tiles_05 = OSM().opts(alpha=0.5, bgcolor='black')
# Convert latitude and longitude to meters that holoviews can understand
df["stop_x"], df["stop_y"] = lnglat_to_meters(df.stop_longitude, df.stop_latitude)
Első lépésként a BME I épület 10 perces sétatávolságban található megállókat kell megtalálni. Ehhez első sorban megállíptjuk a BME I épület koordinátáját (47.4724702,19.0597401). Feltételezhetjük, hogy egy ember normál tempóban 4 km/h sebességgel sétál, valamint a megállókat légvonalban keressük, mivel a ténylegesen gyalogosan megközelíthető útvonalak meghatározása lényegesen bonyolítaná a feladatot. A koordináták közötti távolságot a Haversine formula segítségével határozzuk meg.
## ----- Variables -----
bme_i_lon = 19.0597401
bme_i_lat = 47.4724702
max_minutes = 10 # min
max_speed = 4 # km/h
avg_earth_radius = 6371.0088
## ----- Helpers -----
def distance(lat1, lng1, lat2, lng2):
# convert all latitudes/longitudes from decimal degrees to radians
lat1 = np.radians(lat1)
lng1 = np.radians(lng1)
lat2 = np.radians(lat2)
lng2 = np.radians(lng2)
# calculate haversine
lat = lat2 - lat1
lng = lng2 - lng1
d = np.square(np.sin(lat * 0.5)) + np.cos(lat1) * np.cos(lat2) * np.square(np.sin(lng * 0.5))
return 2 * avg_earth_radius * np.arcsin(np.sqrt(d))
def findNearStops():
# Determine the near stops from BME building I
max_distance = max_speed * max_minutes / 60 # km
df["bme_i_distance"] = distance(bme_i_lat, bme_i_lon, df.stop_latitude, df.stop_longitude)
return df[df["bme_i_distance"] <= max_distance]
## ----- Script -----
near_stops = findNearStops()
near_stops_points = hv.Points(near_stops.drop_duplicates("stop_id"), kdims=["stop_x","stop_y"], vdims=["stop_name", "bme_i_distance"]).opts(color="r",size=10, tools=['hover'], title="Az I épülettől 10 perces sétatávolságban levő megállók")
map_tiles * near_stops_points
Az adathalmaz nem tartalmaz információt arról, hogy egy járat éjszakai-e vagy nem, így feltételezzük, hogy éjszakai járat a BKK meghatározása alapján a 6-os villamos, a 200E busz, illetve a 900-as jelzésű buszok. Tekintettel arra, hogy a 6-os villamos és a 200E busz egész nap közlekedik, tekintsük éjszakai járatnak azokat, melyek 23:00 óra és 04:00 óra között közlekednek, mivel a legtöbb éjszakai busz is hasonló időben jár.
# import bamboolib as bam
# Step: Group by and aggregate
near_routes_list = near_stops.groupby(['route_name','route_type']).agg(route_name_size=('route_name', 'size')).reset_index()
print("Járművek, amelyek megállnak a közeli megállókban: ")
for route in near_routes_list['route_name']:
print(route + ' ',end='')
print('\nEbből villamos:')
for route in near_routes_list[near_routes_list['route_type'] == 0]['route_name']:
print(route + ' ',end='')
print('\nEbből busz:')
for route in near_routes_list[near_routes_list['route_type'] == 2]['route_name']:
print(route + ' ',end='')
Járművek, amelyek megállnak a közeli megállókban: 1 107 133E 153 154 212 212A 212B 33 4 41 6 901 918 Ebből villamos: 1 4 41 6 Ebből busz: 107 133E 153 154 212 212A 212B 33 901 918
# Collect the routes, that are available from the near stops
available_trips_ids = near_stops.trip_id.unique()
available_trips = df[df.trip_id.isin(available_trips_ids)]
# Filter night routes
time_23 = datetime.strptime("23:00", "%H:%M")
time_04 = datetime.strptime("04:00", "%H:%M")
night_lines = available_trips[ (available_trips.route_name.str.match('^9\d\d$')) | (available_trips.route_name.str.match('^(6|200E)$'))]
# BKK specified night lines
night_routes = night_lines[(night_lines.stop_departure_time >= time_23 ) | (night_lines.stop_departure_time <= time_04 )]
# Other lines in the night
routes_at_night = available_trips[(available_trips.stop_departure_time >= time_23 ) | (available_trips.stop_departure_time <= time_04 )]
# Fix datetime representation of stops after midnight
routes_at_night['corrected_stop_departure_time'] = routes_at_night['stop_departure_time'].transform(lambda x: x + timedelta(days=1) if x <= time_04 else x)
routes_at_night['corrected_stop_arrival_time'] = routes_at_night['stop_arrival_time'].transform(lambda x: x + timedelta(days=1) if x <= time_04 else x)
night_routes['corrected_stop_departure_time'] = night_routes['stop_departure_time'].transform(lambda x: x + timedelta(days=1) if x <= time_04 else x)
night_routes['corrected_stop_arrival_time'] = night_routes['stop_arrival_time'].transform(lambda x: x + timedelta(days=1) if x <= time_04 else x)
Globálisan is megfigyelhető, hogy a Budapesti tömegközlekedés leginkább 05:30 és 23:59 között aktív. Ez alátámasztja, hogy éjszakainak tekintjük az ebben az időszakban közlekedő járműveket.
import plotly.express as px
fig = px.histogram(available_trips.sample(n=10000, replace=False, random_state=123).sort_index(), x='stop_departure_time',
title= "Budapesti tömegközlekedés gyakorisága",labels= {'stop_departure_time':'Időpont [30 perc]'})
fig.update_layout(yaxis_title="Darabszám")
fig
Az EDA eszköze esetünkben a pandas_profiling lesz, aminek segítségével gyorsan, és egyszerűen tudunk egy DataFrameről felderítő adatelemzést végezni.
# Quick data EDA with pandas_profiling
from pandas_profiling import ProfileReport
profile = ProfileReport(routes_at_night, title="Nigth Routes near I building",html={'full_width':True})
profile
Summarize dataset: 0%| | 0/5 [00:00<?, ?it/s]
Generate report structure: 0%| | 0/1 [00:00<?, ?it/s]
Render HTML: 0%| | 0/1 [00:00<?, ?it/s]
#Show convergence Matrice in full display
import plotly.graph_objects as go
df_corr = routes_at_night.corr()
fig = go.Figure()
fig.add_trace(
go.Heatmap(x = df_corr.columns,y = df_corr.index,z = np.array(df_corr),text=df_corr.values,texttemplate='%{text:.2f}',
colorbar=dict(thickness=5, tickvals=[-1, 1], ticktext=['-1', '1'], outlinewidth=0),
colorscale='RdBu')
)
fig.layout.width = 800
fig.layout.height = 800
fig.layout.title = "Korrelációs mátrix az adatokról."
fig.show()
Megfigyelhető, hogy mindössze pár éjszakai járat minősül bicikliszállításra alkalmasnak
# Display bike allowed routes
tmp = routes_at_night[routes_at_night['trip_bikes_allowed'] == 1]["route_name"].unique()
print( "Járatok az I közelében, ahol egyáltalán lehetőség van bicikli szállításra:")
for i in tmp:
print(i,end=', ')
Járatok az I közelében, ahol egyáltalán lehetőség van bicikli szállításra: 1, 41, 212,
# Step: Sort column(s) corrected_stop_departure_time ascending (A-Z)
routes_at_night = routes_at_night.sort_values(by=['corrected_stop_departure_time'], ascending=[True])
#supressed bamboolib explorer
routes_at_night
| agency_name | route_name | route_type | route_desc | trip_id | trip_direction | trip_wheelchair_accessible | trip_bikes_allowed | trip_boarding_door | stop_arrival_time | ... | stop_name | stop_latitude | stop_longitude | stop_location_type | stop_wheelchair_boarding | stop_x | stop_y | bme_i_distance | corrected_stop_departure_time | corrected_stop_arrival_time | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 3434742 | BKK | 1 | 0 | Kelenföld vasútállomás M / Bécsi út / Vörösvár... | C56318980 | 1 | 1 | 2 | 1 | 1900-01-01 23:00:00 | ... | Erzsébet királyné útja, aluljáró | 47.516400 | 19.091909 | 0 | 1 | 2.125302e+06 | 6.026775e+06 | 5.449977 | 1900-01-01 23:00:00 | 1900-01-01 23:00:00 |
| 3881290 | BKK | 212 | 2 | Boráros tér H / Normafa, látogatóközpont | C56575834 | 1 | 1 | 1 | 1 | 1900-01-01 23:00:00 | ... | Óra út | 47.504525 | 18.998004 | 0 | 1 | 2.114848e+06 | 6.024818e+06 | 5.850018 | 1900-01-01 23:00:00 | 1900-01-01 23:00:00 |
| 1757746 | BKK | 1 | 0 | Kelenföld vasútállomás M / Bécsi út / Vörösvár... | C53761157 | 0 | 1 | 2 | 1 | 1900-01-01 23:00:00 | ... | Hengermalom út / Szerémi út | 47.462551 | 19.048509 | 0 | 1 | 2.120470e+06 | 6.017904e+06 | 1.388976 | 1900-01-01 23:00:00 | 1900-01-01 23:00:00 |
| 3881250 | BKK | 212 | 2 | Boráros tér H / Normafa, látogatóközpont | C56575830 | 1 | 1 | 1 | 1 | 1900-01-01 23:00:00 | ... | Petőfi híd, budai hídfő | 47.476573 | 19.059268 | 0 | 1 | 2.121668e+06 | 6.020213e+06 | 0.457589 | 1900-01-01 23:00:00 | 1900-01-01 23:00:00 |
| 3881206 | BKK | 212 | 2 | Boráros tér H / Normafa, látogatóközpont | C56575827 | 0 | 1 | 1 | 1 | 1900-01-01 23:00:00 | ... | Sirály utca | 47.490555 | 19.018489 | 0 | 2 | 2.117129e+06 | 6.022516e+06 | 3.695090 | 1900-01-01 23:00:00 | 1900-01-01 23:00:00 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 1717002 | BKK | 1 | 0 | Kelenföld vasútállomás M / Bécsi út / Vörösvár... | C537456119J | 1 | 2 | 1 | 1 | 1900-01-01 04:00:00 | ... | Hős utca | 47.495706 | 19.109078 | 0 | 1 | 2.127213e+06 | 6.023365e+06 | 4.518971 | 1900-01-02 04:00:00 | 1900-01-02 04:00:00 |
| 2832953 | BKK | 918 | 2 | Kelenföld vasútállomás M / Óbudai autóbuszgarázs | C55577866 | 1 | 1 | 2 | 1 | 1900-01-01 04:00:00 | ... | Kacsóh Pongrác út | 47.517806 | 19.089562 | 0 | 2 | 2.125040e+06 | 6.027007e+06 | 5.516584 | 1900-01-02 04:00:00 | 1900-01-02 04:00:00 |
| 3470049 | BKK | 6 | 0 | Széll Kálmán tér M / Móricz Zsigmond körtér M | C5634241654 | 1 | 1 | 2 | 0 | 1900-01-01 04:00:00 | ... | Mester utca / Ferenc körút | 47.482751 | 19.068848 | 0 | 1 | 2.122734e+06 | 6.021230e+06 | 1.332436 | 1900-01-02 04:00:00 | 1900-01-02 04:00:00 |
| 3872160 | BKK | 901 | 2 | Kelenföld vasútállomás M / Bécsi út / Vörösvár... | C56568102 | 0 | 1 | 2 | 0 | 1900-01-01 04:00:00 | ... | Népliget M | 47.476330 | 19.099513 | 0 | 2 | 2.126148e+06 | 6.020173e+06 | 3.019942 | 1900-01-02 04:00:00 | 1900-01-02 04:00:00 |
| 3872729 | BKK | 901 | 2 | Kelenföld vasútállomás M / Bécsi út / Vörösvár... | C5656864 | 1 | 1 | 2 | 0 | 1900-01-01 04:00:00 | ... | Albert Flórián út | 47.472525 | 19.095428 | 0 | 1 | 2.125693e+06 | 6.019546e+06 | 2.682368 | 1900-01-02 04:00:00 | 1900-01-02 04:00:00 |
37489 rows × 22 columns
Ha megvizsgáljuk az I környékéről induló járatokat, akkor megfigyelhető egy egyértelmű vágás, ami nagyjából éjfél környékén kezdődik. Ezután már az éjszakai járatok dominálnak, amiknek a járatsűrűsége lényegesen kisebb. Hisztogramon ábrázolva egyértelműen látszik, hogy a 133E, 4-es villamos, 1-es villamos, 212-es busz csak éjfélig járnak, ezután már leginkább csak dedikált éjszakai járatok: a 6, 901 és 918 járatok járnak.Az is látszik, hogy a 901-es buszok megközelítőleg kerek időpontokban, félóránként járnak az I környéki megállókban.
import plotly.express as px
dat = routes_at_night.sample(n=10000, replace=False, random_state=123).sort_index()
fig = px.histogram(dat[dat['stop_id'].isin(near_stops['stop_id'])], x='corrected_stop_departure_time',color='route_name',
color_discrete_sequence = px.colors.qualitative.T10 ,barmode='relative',title="Éjszaka 23:00 és 4:00 közötti járatok amelyek az I épület környékén indulnak.",
labels={'corrected_stop_departure_time':'Indulás időpontja [25 perc]', 'route_name':'Járat'})
fig.update_layout(yaxis_title="Darabszám")
fig
Jól megfigyelhető a periódikusság az I közeli megállókon, amit a végighaladó buszok alakítanak ki.
import plotly.express as px
dat = night_routes.sample(n=10000, replace=False, random_state=123).sort_index()
fig = px.histogram(dat[dat['stop_id'].isin(near_stops['stop_id'])], x='corrected_stop_arrival_time',
color='route_name',barmode='overlay',title="I környéki éjszakai járatok sűrűségfüggvényei",
labels={'count':'db','corrected_stop_arrival_time':'Indulás időpontja [25 perc]', 'route_name': 'Járat'})
fig.update_layout(yaxis_title="Darabszám")
fig
Az éjszakai járatok kategórikus adatainak kissé unortodox módja lehet egy folyam diagram: látható, hogy a járatok jelentős része a 6-os villamos, ami az egyetlen elérhető villamos. Jellemzően első ajtós felszállással múködik az összes éjszakai busz, bár az adat pontossága megkérdőjelezhető, hiszan első ajtós felszállás nem jellemző a villamosokon.
import plotly.express as px
paralell_data = night_routes.sample(n=10000, replace=False, random_state=123).sort_index()
paralell_data["color"] = paralell_data.route_name.map({"6":"#ffd800", "901":"#000000", "918":"#75597B"})
paralell_data["route_type"] = paralell_data.route_type.map({0:"villamos", 2:"busz"})
paralell_data["trip_direction"] = paralell_data.trip_direction.map({0:"normál", 1:"ellentétes"})
paralell_data["trip_boarding_door"] = paralell_data.trip_boarding_door.map({0:"bármelyik", 1:"első ajtó"})
fig = px.parallel_categories(paralell_data, dimensions=['route_name', 'route_type', 'trip_direction', 'trip_boarding_door'],
labels={'route_name':'Útvonal neve', 'route_type':'Jármű típusa', 'trip_direction':'Járat iránya', 'trip_boarding_door':'Megengedett felszállási ajtó'}
, color="color",title="Éjszakai járatok főbb tulajdonságai")
fig
A vizualizációhoz első lépésben létrehoztunk egy grid-et, ami Budapest közigazgatási határain belüli pontokat tartalmazza. A szükséges utazái idők meghatározását az egyes grid pontokra 2 lépésben határoztuk meg. Első lépésben vizsgáltuk a triviális esetet, hogy mennyi idő elsétálni (a korábbiakhoz hasonlóan légvonalban) az adott pontokhoz az I épülettől. A következő lépésben ugyanezekre a pontokra meghatároztuk azt is, hogy mennyi idő eljutni tömegközlekedéssel (több opció esetén vettük a minimumot) és végül a két idő közül vettük a kisebbet. A triviális esetet azért volt szükséges vizsgálni, mert az egyetemhez közeli területeken számos olyan pont van, ahova gyorsabb elsétálni, mint tömegközlekedéssel elmenni.
A két idő meghatározása közül a tömegközlekedéses verzió, ami összetettebb. Ez is több lépésben valósult meg. Első lépésben meghatároztuk, hogy az egyes megállókhoz mennyi idő elsétálni, majd megkerestük azokat a járatokat, melyek ez az időpont (este 10 + sétálással eltelt idő) után érkeznek a megállóba. Összesítettük, hogy ezek a járatok segítségével melyik másik megállókba lehet eljutni, és mikorra érkezünk meg velük, majd szűrtük, hogy minden megállóhoz csak a legkorábbi érkezési időt tartottuk meg. Ezzel megkaptuk, hogy este 10-es indulással mikorra lehet eljutni a város különböző pontjaira. Ezután az utolsó feladat az volt, hogy meghatározzuk, hogy ideális esetben az egyes grid pontokhoz mennyi idő eljutni. Ehhez azt kellet, hogy kiszámoljuk, hogy az egyes grid ponthoz, mennyi idő eljutni az egyes megállókból, kiegészítve az oda utazási időkkel és ezek közül választottuk a legkisebbet.
Végül az eredményeket vizualizáltuk egy heatmapen.
import numpy as np
from math import ceil
def visualization():
# Determine the minimum and maximum coordinates
min_lon, max_lon = min(budapest.lon), max(budapest.lon)
min_lat, max_lat = min(budapest.lat), max(budapest.lat)
# Create grid
resolution = 150
range_lon = np.linspace(min_lon, max_lon, resolution)
range_lat = np.linspace(min_lat, max_lat, resolution)
heatmap = pd.DataFrame({"lat": range_lat}).join(pd.DataFrame({"lon": range_lon}), how='cross')
heatmap["x"], heatmap["y"] = lnglat_to_meters(heatmap.lon, heatmap.lat)
# Determine points inside of Budapest
heatmap = heatmap[budapest_border.contains_points(heatmap[["x", "y"]])]
# Calculate walk time
heatmap["walk_min"] = distance(bme_i_lat, bme_i_lon, heatmap.lat, heatmap.lon) / max_speed * 60
# Determine when you can get to each stops on walk
t = datetime.strptime("22:00", "%H:%M")
df["bme_i_walk"] = df.bme_i_distance.apply(lambda d: t + timedelta(hours=d / max_speed))
# Filter the available trips based on the departure time
walk = df[df.stop_departure_time >= df.bme_i_walk][["trip_id", "stop_id", "stop_departure_time"]]
# Find the available arrival stops
stops = walk.merge(df[["trip_id","stop_id", "stop_arrival_time", "stop_longitude", "stop_latitude", "stop_x", "stop_y"]], left_on="trip_id", right_on="trip_id", suffixes=["_from", "_to"])
stops = stops[(stops.stop_id_from != stops.stop_id_to) & (stops.stop_arrival_time > stops.stop_departure_time)]
# Find the earliest time you can get each stop and calculate the required time
stops = stops.groupby(['stop_id_to'], as_index=False).apply(lambda x: x.iloc[x.stop_departure_time.argmin(),])
stops["bme_i_min"] = stops.stop_arrival_time.apply(lambda at: (at-t).total_seconds() / 60)
stops = stops.reset_index()[["stop_longitude", "stop_latitude", "stop_x", "stop_y", "bme_i_min"]].drop_duplicates()
# Find the stop for every grid point from where you can get to there at earliest and calculate that time
heatmap = heatmap.join(stops, how="cross")
heatmap["bkk_min"] = distance(heatmap.lat, heatmap.lon,heatmap.stop_latitude, heatmap.stop_longitude)/max_speed*60 + heatmap.bme_i_min
heatmap = heatmap.groupby(["lat", "lon"], as_index=False).apply(lambda x: x.iloc[x.bkk_min.argmin(),])
# Determine the minimum time (choose between walk or travel + walk)
heatmap["total_min"] = heatmap[["bkk_min","walk_min"]].min(axis=1)
# Determine sampling for holoview
min_x, min_y = lnglat_to_meters(min_lon,min_lat)
max_x, max_y = lnglat_to_meters(max_lon,max_lat)
x_sampling = ceil((max_x-min_x)/resolution)
y_sampling = ceil((max_y-min_y)/resolution)
# Display heatmap
hv_heatmap = hv.HeatMap(heatmap, kdims=["x", "y"], vdims=["total_min"])
stream = [hv.streams.RangeXY(source=hv_heatmap)]
hv_heatmap_agg = regrid(aggregate(hv_heatmap, aggregator=ds.min('total_min'), x_sampling=x_sampling, y_sampling=y_sampling, streams=stream), upsample=True, interpolation='linear').opts(cmap='RdYlGn_r', alpha=0.5, colorbar=True, clabel='Utazási idő (perc)', clipping_colors={'NaN':'transparent'})
return map_tiles_05*hv_heatmap_agg
hv_heatmap = visualization()
hv_heatmap.opts(hv.opts.Overlay(title='I épülettől számított utazási távolság'))
hv_heatmap
A heatmapen jól kivehetőek a főbb tömegközlekedési vonalak. Például sötét zöld szín mutatja a 4-6-os illetve az 1-es villamos vonalát, mivel ezek mind gyakran járnak és közel vannak az I épülethez. Ezen felül még meghatározó a 33-as és 133E jelzésű busz vonala, illetve az egyéb, pl 17-es villamos vonala. Ami még jól látszik a térképen, hogy Budapest tömegközlekedési hálózata nyilvánvalóan nem egyenletes. Például egész jól kivehető, hogy a reptérre viszonylag gyorsan, kb 60 perc alatt ki lehet jutni az egyetemtől, viszont ha egy kicsit arrébb mennénk, pl Rákoshegyre, akkor ennek duplájára van szükség, annak ellenére, hogy távolságban közelebb van.
A kapott heatmapet tovább vizsgálhatjuk, ha kombináljuk az elérhető kocsmákkal:
hv_places = hv.Points(bars, kdims=["x", "y"], vdims=["name", "vicinity", "rating"],label='kocsmák').opts(color="blue",size=8, tools=['hover'])
bar_plot = hv_heatmap*hv_places
bar_plot.opts(hv.opts.Overlay(title='I épülettől számított utazási távolság percben + kocsmák'))
bar_plot
A kocsmák tekintetében látható, hogy ezek leginkább a körúton belül helyezkednek el, és ezen kívül lényegesen kevesebb található. Továbbá érdekes asszimetriát mutat, hogy Pesten sokkal több található, mint Budán, bár ez feltehetően a Gellért hegy miatt is lehet. A leggyorsabban elérhetőek az egyetem közvetlen közelében találhatóak, például a Budafoki, Bartók Béla és Karinthy Frigyes úton, de egész sokat lehet találni még a Duna túloldalán, a Boráros tér közelében is.
Az elérhető megállók megtalálását k darab átszállással hasonló képpen tudjuk megcsinálni, mint a korábbi feladatban. Feltételezzük, hogy maximum 10 percet sétálhatunk egyszerre. Így első lépésben vesszük azokat a megállókat, amik elérhetőek 10 perc sétán belül az I épülethez, majd megkeressük azokat a megállókat, ahova el lehet jutni tömegközlekedéssel ezekből a megállókból, majd végül még vizsgáljuk azokat a megállókat, amelyekbe el tudunk jutni ezekből 10 percen belül. Mindegyik esetben végig vizsgáljuk, hogy ne fussunk ki az előírt időből. A k>1 esetben ugyanezt iteráljuk tovább, tehát a korábbi esetben megkapott megállókhoz vesszük azokat, amelyekhez el tudunk jutni tömegközlekedéssel, valamint vesszük azokat, amelyek gyalog megközelíthetőek innen. A vizualizáción a k = 0, 1 és 2 eseteket láthatjuk.
def findStopsK(k):
t_22_00 = datetime.strptime("22:00", "%H:%M")
t_22_45 = datetime.strptime("22:45", "%H:%M")
min10 = 10
t_min10 = timedelta(minutes=min10)
# Find the available stops on foot
stops_available = df[df.bme_i_walk-t_22_00<=t_min10][["stop_id", "bme_i_walk"]].drop_duplicates()
stops_available = stops_available.rename(columns={'bme_i_walk': 'end_time'})
all_available = stops_available.stop_id.unique()
# Find available options within timeframe
df_in_time = df[["trip_id", "stop_id", "stop_longitude", "stop_latitude", "stop_x", "stop_y", "stop_departure_time", "stop_arrival_time"]][(df.stop_departure_time >= t_22_00) & (df.stop_arrival_time <= t_22_45)]
# Find travel options between stops
options = df_in_time[['trip_id','stop_id','stop_departure_time']]\
.set_index('trip_id')\
.join( df_in_time[['trip_id', 'stop_id', 'stop_arrival_time']].set_index('trip_id'), lsuffix='_from', rsuffix='_to')\
.reset_index()
options = options[options.stop_departure_time < options.stop_arrival_time][['trip_id', 'stop_id_from', 'stop_departure_time', 'stop_arrival_time','stop_id_to']]
# Find walk connections between stops
connections = df[["stop_id", "stop_longitude", "stop_latitude"]].drop_duplicates()
connections = connections.join(connections, how="cross", lsuffix="_from", rsuffix="_to")
# connections = connections[connections.stop_id_from == connections.stop_id_to] # if we do not want to count the options when walk from one stop to other
connections["walk_min"] = distance(connections.stop_latitude_from, connections.stop_longitude_from, connections.stop_latitude_to, connections.stop_longitude_to) / max_speed * 60
connections = connections[connections.walk_min <= min10][["stop_id_from","stop_id_to", "walk_min"]]
connections["walk_min"] = connections.walk_min.apply(lambda t: timedelta(minutes=t))
for _ in range(k+1):
# Travel from one stop to other
stops_available = stops_available.merge(options, left_on="stop_id", right_on="stop_id_from")
stops_available = stops_available[stops_available.end_time < stops_available.stop_departure_time]
stops_available = stops_available[["stop_id_to", "stop_arrival_time"]].drop_duplicates()
stops_available = stops_available.rename(columns={'stop_id_to': 'stop_id'})
all_available = np.unique(np.append(all_available,stops_available.stop_id.unique()))
# Walk from the stop
stops_available = stops_available.merge(connections, left_on="stop_id", right_on="stop_id_from")
stops_available["end_time"] = stops_available.stop_arrival_time + stops_available.walk_min
stops_available = stops_available[stops_available.end_time <= t_22_45][["stop_id_to", "end_time"]].drop_duplicates()
stops_available = stops_available.rename(columns={'stop_id_to': 'stop_id'})
all_available = np.unique(np.append(all_available,stops_available.stop_id.unique()))
stops = pd.DataFrame({"stop_id": all_available}).set_index("stop_id").join(df[["stop_id", "stop_x", "stop_y", "stop_name"]].set_index("stop_id").drop_duplicates()).reset_index()
return stops
# Filter lists to visualize points individually
hv_stops_k0 = hv.Points(findStopsK(0), kdims=["stop_x","stop_y"], vdims=["stop_name"], label="k=0").opts(color='g',size=10, tools=['hover'])
hv_stops_k1 = hv.Points(findStopsK(1), kdims=["stop_x","stop_y"], vdims=["stop_name"], label="k=1").opts(color='y',size=9, tools=['hover'])
hv_stops_k2 = hv.Points(findStopsK(2), kdims=["stop_x","stop_y"], vdims=["stop_name"], label="k=2").opts(color='r',size=8, tools=['hover'])
res = map_tiles * hv_stops_k2 * hv_stops_k1 * hv_stops_k0
res.opts(hv.opts.Overlay(title='I épülettől elérhető megállók k átszállással'))
res
Az ábrán látható, hogy az átszállás nélküli és 1 átszállással megközelíthető megállók száma jelentős különbséget mutat, viszont ehhez képest a 2 átszállásos megállók már nincsenek jelentősen többen. Ennek feltehető oka, hogy a rendelkezésre álló 45 perces időkeret itt már sokkal jobban korlátozza az utazást. Az átszállás nélküli utazásokat jelentősen a 4-6-os villamos és az 1-es villamos határozza meg, a több átszálássnál már érződik a buszok és metrók hatása is. 2 átszállással Budapest belvárosának nagy részét meg lehet közelíteni, de a külvárosi régiók eléréséhez már nem elég a rendelkezésre álló időkeret.